﻿using SharpDX.Multimedia;
using SharpDX.XAudio2;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace SoundSynth
{
    public class SynthVoice
    {
        public static XAudio2 xAudio2 { get; private set; }
        public static MasteringVoice masteringVoice { get; private set; }
        public static  List<SourceVoice> Voices { get; private set; }
        public static List<SynthVoice> SynthVoices { get; private set; }
        public static void StopAll()
        {
            foreach (var v in SynthVoices) v.StopNote();
        }
        static SynthVoice()
        {
            try
            {
                xAudio2 = new XAudio2();
                masteringVoice = new MasteringVoice(xAudio2);
                Voices = new List<SourceVoice>();
                SynthVoices = new List<SynthVoice>();
            }
            catch (Exception ex)
            {
                var msg = ex.Message;
            }

        }

        public SynthVoice()
        {

                PlayingPitch = -1;
                StopDone = new ManualResetEvent(false);
                CancelStop = new ManualResetEvent(false);
                SynthVoices.Add(this);
           
        }

        public bool IsPlaying { get; set; }

        private int PlayingPitch { get; set; }
        private IInstrument Instrument { get; set; }
        private float ActualFrequencyRatio { get; set; }
        private SourceVoice Voice { get; set; }

        /// <summary>
        /// Load an instrument
        /// </summary>
        /// <param name="instr">Sample instrument, or synthesis instrument</param>
        public void LoadInstrument(IInstrument instr)
        {
            if (Voice != null) Destroy();
                Instrument = instr;
                Voice = new SourceVoice(xAudio2, new WaveFormat(), VoiceFlags.None, 16, false);
                ReloadBuffer(1,true);
                Voices.Add(Voice);
                Voice.SetVolume(1, XAudio2.CommitNow);
                ActualFrequencyRatio = 1;
                PlayingPitch = -1;
        }

        /// <summary>
        /// Play a sound from the sample file
        /// </summary>
        /// <param name="sampleIndex">sample index (starting from zero) in the file</param>
        public void PlaySound(int sampleIndex)
        {
            var asy = Task.Run(() =>
            {

                if ((Voice == null) || (Instrument == null)) return;

                ReloadBuffer(sampleIndex);
                CancelStop.Set();
                //Set the frequency ratio to the voice
                Voice.SetFrequencyRatio(1);


                //Start the play if needed
                if (!IsPlaying)
                {
                    Voice.Start();
                    Voice.SetVolume(1, XAudio2.CommitNow);
                }

                //update state
                IsPlaying = true;
            });
        }

        /// <summary>
        /// Play a sound from the sample file
        /// </summary>
        /// <param name="sampleIndex">sample index (starting from zero) in the file</param>
        public void PlaySound(float frequency)
        {
            var asy = Task.Run(() =>
            {

                if ((Voice == null) || (Instrument == null)) return;

               
                //Get the frequenc y of the pitch
                var FrequencyRatio = frequency / 440f;
                //See if we should use another sample
                var reloadNeeded = Instrument.ReloadNeeded(FrequencyRatio, ActualFrequencyRatio);
                if (Instrument.IsPlugged || reloadNeeded || !IsPlaying || stopping)
                {
                    ReloadBuffer(FrequencyRatio, reloadNeeded);
                }
                CancelStop.Set();
                //Set the frequency ratio to the voice
                var realFrequencyRatio = Instrument.GetCorrectFrequencyRatio(FrequencyRatio);
                Voice.SetFrequencyRatio(realFrequencyRatio);
                ActualFrequencyRatio = FrequencyRatio;
                PlayingPitch = -1;
             

                //Start the play if needed
                if (!IsPlaying)
                {
                    Voice.Start();
                    Voice.SetVolume(1, XAudio2.CommitNow);
                }

                //update state
                IsPlaying = true;
            });
        }

        /// <summary>
        /// Start to play a note, It will be stopped until you call "StopNote"
        /// </summary>
        /// <param name="notePitch"></param>
        public void PlayNote(int notePitch)
        {
            var asy = Task.Run(() =>
           {
               
               if ((Voice == null) || (Instrument == null)) return;

               //If we play another note
               if ((notePitch != PlayingPitch) || !IsPlaying || stopping)
               {
                   //Get the frequenc y of the pitch
                   var FrequencyRatio = XAudio2.SemitonesToFrequencyRatio(notePitch) / 2f;
                   //See if we should use another sample
                   var reloadNeeded = Instrument.ReloadNeeded(FrequencyRatio, ActualFrequencyRatio);
                   if (Instrument.IsPlugged || reloadNeeded||!IsPlaying||stopping)
                   {
                       ReloadBuffer(FrequencyRatio,reloadNeeded);
                   }
                   CancelStop.Set();
                   //Set the frequency ratio to the voice
                   var realFrequencyRatio = Instrument.GetCorrectFrequencyRatio(FrequencyRatio);
                   Voice.SetFrequencyRatio(realFrequencyRatio);
                   ActualFrequencyRatio = FrequencyRatio;
                   PlayingPitch = notePitch;
               }

               //Start the play if needed
               if (!IsPlaying)
               {
                   Voice.Start();
                   Voice.SetVolume(1, XAudio2.CommitNow);
               }

               //update state
               IsPlaying = true;
           });
        }
        /// <summary>
        /// Play a note with a duration
        /// </summary>
        /// <param name="notePitch">Semitone number</param>
        /// <param name="duration">Duration in milliseconds</param>
        /// <returns></returns>
        public async Task PlayNote(int notePitch, int duration)
        {
          await Task.Run(() =>
          {
              PlayNote(notePitch);
              new ManualResetEvent(false).WaitOne(duration);
              StopNote();
          });
        }

        /// <summary>
        /// Silence
        /// </summary>
        /// <param name="duration">Duration in milliseconds</param>
        /// <returns></returns>
        public async Task PlaySilence(int duration)
        {
            await Task.Run(() =>
            {
                StopNote();
                new ManualResetEvent(false).WaitOne(duration);
                StopNote();
            });
        }

        //Reload the instrument sample
        AudioBuffer actualbuffer;
        uint[] bufferinfo;
        private void ReloadBuffer(float frequencyRatio,bool reloadNeeded)
        {
            if ((actualbuffer == null) || reloadNeeded)
            {
                SoundStream stream = new SoundStream(Instrument.GetDataStream(frequencyRatio));
                actualbuffer = new AudioBuffer()
                {
                    Stream = stream,
                    AudioBytes = (int)stream.Length,
                    Flags = BufferFlags.EndOfStream,
                    PlayBegin = 0,
                    PlayLength = 0,
                    LoopCount = (Instrument.IsPlugged) ? 0 : 255
                };
                bufferinfo = stream.DecodedPacketsInfo;
            }
            if (IsPlaying) Voice.Stop(XAudio2.CommitNow);
            Voice.FlushSourceBuffers();
            Voice.SubmitSourceBuffer(actualbuffer, bufferinfo);
            if (IsPlaying) Voice.Start(XAudio2.CommitNow);
        }

        private void ReloadBuffer(int sampleIndex)
        {
            SoundStream stream = new SoundStream(Instrument.GetDataStream(sampleIndex));
            actualbuffer = new AudioBuffer()
            {
                Stream = stream,
                AudioBytes = (int)stream.Length,
                Flags = BufferFlags.EndOfStream,
                PlayBegin = 0,
                PlayLength = 0,
                LoopCount = 255
            };
             bufferinfo = stream.DecodedPacketsInfo;
            if (IsPlaying) Voice.Stop(XAudio2.CommitNow);
            Voice.FlushSourceBuffers();
            Voice.SubmitSourceBuffer(actualbuffer, bufferinfo);
            if (IsPlaying) Voice.Start(XAudio2.CommitNow);
        }

        ManualResetEvent CancelStop;
        ManualResetEvent StopDone;
        bool stopping;
        /// <summary>
        /// Stop the note with a fadeout
        /// We can gives him a callback to execute when the note is stoped
        /// </summary>
        /// <param name="callback">Callback to exececute when the fadeout is done</param>
        public void StopNote(Action callback=null)
        {
            if (stopping) return;
            if ((Voice == null) || (Instrument == null) || (!IsPlaying)) return;
            stopping = true;
            CancelStop.Reset();
            var asy = Task.Run(() =>
            {
                /*
                Voice.Stop(PlayFlags.Tails, XAudio2.CommitNow);
                IsPlaying = false;
                PlayingPitch = -1;
                if (callback != null) callback();
                return;
                */
                //fadeout loop
                for (var i = Voice.Volume; (i >= 0.15) && (!CancelStop.WaitOne(20)); i -= 0.05f)
                {
                    Voice.SetVolume(i);
                }
                //If the stop has not been canceled
                if (!CancelStop.WaitOne(1))
                {
                    Voice.Stop(PlayFlags.Tails, XAudio2.CommitNow);
                    IsPlaying = false;
                    PlayingPitch = -1;
                    if (callback != null) callback();
                }
                else
                {
                    Voice.SetVolume(1);
                }
                StopDone.Set();
                stopping = false;
            });
        }
        /// <summary>
        /// Stop the playback and flush the voice buffer
        /// </summary>
        public void Destroy()
        {
            //Stop and flush the voice
            CancelStop.Set();
           
            Voice.Stop();
            Voice.FlushSourceBuffers();
            Voices.Remove(Voice);
            ActualFrequencyRatio = 0;
            //Voice.Dispose();
            //Voice = null;
        }
    }
}
